from math import inf, isinf
from typing import Set, Tuple, List, Dict, Optional, Union

import networkx as nx

from model.action import Action


class Topo(nx.Graph):
    def __init__(self):
        super().__init__()
        self.physical_nodes = None
        self.physical_edges = None
        self.service_nodes = None
        self.service_edges = None
        self.app = None
        self.cptem_info = None
        self.app_SFC = None
        self.deploy_info = None

    def initial_topo(self,
                     physical_nodes: Dict[Union[str, Tuple[str, str]], dict],
                     physical_edges: Dict[Tuple[str, str], Dict[str, float]],
                     service_nodes: Set[str],
                     deploy_info: Dict[str,List[str] | str],
                     app_sfc: Dict[str, List[str]],
                     cptem_info: Dict[Union[str, Tuple[str, str]], Optional[str]]
                     ):
        self.physical_nodes = physical_nodes
        self.physical_edges = physical_edges
        self.service_nodes = service_nodes
        # self.service_edges = service_edges
        self.deploy_info = deploy_info
        self.app_SFC = app_sfc
        self.cptem_info = cptem_info

        self.add_nodes_from(physical_nodes, weight = 1.0)
        # self.add_edges_from(physical_edges)
        self.add_weighted_edges_from([(edge[0], edge[1], attr['weight']) for edge, attr in physical_edges.items()])
        self.add_nodes_from(service_nodes)

        # self.service_edges = self.update_service_edge()
        self.update_service_edge()
        self.add_edges_from(self.service_edges)

        self.update_application()



    def get_topo_state(self):
        tf = {"physical_nodes": self.physical_edges, "service_nodes": self.service_nodes,
                     "service_edges": self.service_edges, "deploy_info": self.deploy_info, "app_sfc": self.app_SFC,
                     "cptem_info": self.cptem_info}
        return tf

    def edge_fail(self, edge: Tuple[str, str]):
        self.edges[edge]['weight'] = inf
    
    def edge_repair(self, edge: Tuple[str, str]):
        self.edges[edge]['weight'] = self.physical_edges[edge]['weight']

    def node_fail(self, node: str):
        self.nodes[node]['weight'] = inf

    def node_repair(self, node: str):
        self.nodes[node]['weight'] = self.physical_nodes[node]['weight']

    def update_service_edge(self):
        app_sfc = self.app_SFC
        service_edges_paths = {}
        for app, sfc in app_sfc.items():
            for i in range(len(sfc) - 1):
                source_service_node = sfc[i]
                target_service_node = sfc[i + 1]
                (physical_s_nodes, physical_t_nodes) = (
                self.deploy_info[source_service_node], self.deploy_info[target_service_node])

                paths_between_physical_nodes = {}
                """
                {
                    ('n1', 'n2'): {
                        ('n1', 'n2'): 3.0, 
                        ('n1', 'n3', 'n2'): 5.0, 
                        ('n1', 'n3', 'n4', 'n2'): 7.0
                    },
                    ('n1', 'n3'): {
                        ('n1', 'n2', 'n3'): 5.0, 
                        ('n1', 'n2', 'n4', 'n3'): 7.0, 
                        ('n1', 'n3'): 3.0
                    }
                }
                """
                match (physical_s_nodes, physical_t_nodes):
                    case sns, tns if (type(sns) == list and type(tns) == list):
                        for sn in sns:
                            for tn in tns:
                                self.cal_paths_between_sn_tn(paths_between_physical_nodes, sn, tn)
                    case sn, tns if (type(sn) == str and type(tns) == list):
                        for tn in tns:
                            self.cal_paths_between_sn_tn(paths_between_physical_nodes, sn, tn)
                    case sns, tn if (type(sns) == list and type(tn) == str):
                        for sn in sns:
                            self.cal_paths_between_sn_tn(paths_between_physical_nodes, sn, tn)
                    case sn, tn if (type(sn) == str and type(tn) == str):
                        self.cal_paths_between_sn_tn(paths_between_physical_nodes, sn, tn)
                    case _:
                        raise ValueError("Invalid deploy info")
                # self.service_edges[(source_service_node, target_service_node)] = paths_between_physical_nodes
                service_edges_paths[source_service_node, target_service_node] = paths_between_physical_nodes
        self.service_edges = service_edges_paths

    def update_application(self):
        self.app = {}
        for app, sfc in self.app_SFC.items():
            sfc_paths_length = {}
            for i in range(len(sfc) - 1):
                source_service_node = sfc[i]
                target_service_node = sfc[i + 1]



                paths_dict = self.service_edges[(source_service_node, target_service_node)]
                for (source_physical_node, target_physical_node), paths_length_dict in paths_dict.items():
                    # 服务节点是否故障
                    if isinf(self.nodes[source_physical_node]["weight"]) or isinf(self.nodes[target_physical_node]["weight"]) :
                        continue

                    if not (source_physical_node in sfc_paths_length.keys()):
                        sfc_paths_length[target_physical_node] = paths_length_dict
                    else:
                        # 把路径接上
                        temp = sfc_paths_length.pop(source_physical_node)
                        temp1 ={}
                        for k1, v1 in paths_length_dict.items():
                            for k, v in temp.items():
                                key = k + k1[1:]
                                value = v + v1
                                temp1[key] = value


                        sfc_paths_length[target_physical_node] = temp1
            dr = {}
            for d in sfc_paths_length.values():
                # 消除路径中的重复元素
                # if len(d.keys()) == len(set(d.keys())):
                #     dr.update(d)
                for k, v in d.items():
                    if len(k) == len(set(k)):
                        dr[k] = v
            self.app[app] = dr



    def update_for_action(self, act: Action, attr: Optional[Dict] = None) -> Union["Topo", Dict]:
        entity_type = act.action_entity_type()
        match entity_type:
            case "node":
                if act.act_name == "fail":
                    self.node_fail(act.entity)
                    self.update_service_edge()
                elif act.act_name == "repair":
                    self.node_repair(act.entity)
                    self.update_service_edge()
            case "physical edge":
                if act.act_name == "fail":
                    self.edge_fail(act.entity)
                    self.update_service_edge()
                elif act.act_name == "repair":
                    self.edge_repair(act.entity)
                    self.update_service_edge()
            case "service":
                working_nodes = attr["working_nodes"]
                deploy_nodes = self.deploy_info[act.entity]
                match (working_nodes, deploy_nodes):
                    case wns, dns if (type(wns) == dict and type(dns) == list):
                        unworking_nodes = set(dns) - set(wns)
                    case wns, dns if (type(wns) == dict and type(dns) == str):
                        unworking_nodes = {dns} - set(wns)
                    case wns, dns if (type(wns) == set and type(dns) == list):
                        unworking_nodes = set(dns) - wns
                    case wns, dns if (type(wns) == set and type(dns) == str):
                        unworking_nodes = {dns} - wns
                    case _:
                        raise ValueError("Invalid deploy info")
                flag = False
                for node in unworking_nodes:
                    if not isinf(self.nodes[node]["weight"]):
                        self.node_fail(node)
                        flag = True
                for node in working_nodes:
                    if self.nodes[node]["weight"] != self.physical_nodes[node]["weight"]:
                        self.node_repair(node)
                        flag = True
                if flag:
                    self.update_application()
                return attr
            case "service edge":
                self.update_application()
            case "application":
                pass
            case _:
                pass
        return self

    def cal_paths_between_sn_tn(self, paths_between_physical_nodes, sn, tn):
        paths = nx.all_simple_paths(self, sn, tn)
        path_lengths = {}
        for path in paths:
            path_length = 0
            for i in range(len(path) - 1):
                node_weight = self.nodes[path[i]]['weight']
                path_length += node_weight
                edge_weight = self.get_edge_data(path[i], path[i + 1])['weight']
                path_length += edge_weight
            path_length += self.nodes[path[-1]]['weight']
            if not isinf(path_length):
                path_lengths[tuple(path)] = path_length
        paths_between_physical_nodes[(sn, tn)] = path_lengths

    def if_service_edges_changed(self, act: Action, service_edge: Tuple[str, str],
                                 old_paths: Dict[Tuple[str, str], Dict[Tuple[str, str], float]]) -> bool:

        if not ((act.action_entity_type() == "node") or (act.action_entity_type() == "physical edge")):
            return False

        new_paths = self.path_of_service_edge(service_edge)
        return not dict_equal(new_paths, old_paths)


    def path_of_service_edge(self, service_edge: Tuple[str, str]) -> Dict[Tuple[str, str], Dict[Tuple[str, str], float]]:
        """


        :param service_edge:
        :return:
        pahts = {
                    ('n1', 'n2'): {
                                    ('n1', 'n2'): 3.0,
                                    ('n1', 'n3', 'n2'): 5.0,
                                    ('n1', 'n3', 'n4', 'n2'): 7.0
                                },
                    ('n1', 'n3'): {
                                    ('n1', 'n2', 'n3'): 5.0,
                                    ('n1', 'n2', 'n4', 'n3'): 7.0,
                                    ('n1', 'n3'): 3.0
                                }
            }
        """
        return self.service_edges[service_edge]

    def if_app_changed(self, act: Action, app_name: str, old_paths) -> bool:
        if not ((act.action_entity_type() == "service") or (act.action_entity_type() == "service edge")):
            return False

        new_paths = self.path_of_app(app_name)
        return not dict_equal(new_paths, old_paths)

    def path_of_app(self, app_name: str):

        """

        :param app_name:
        :return:


        """
        return self.app[app_name]


    def mlag(self):
        # physical_nodes = {"n1", "n2", "n3", "n4", "n5"}
        physical_nodes = {
            "n1":{"weight": 1},
            "n2": {"weight": 1},
            "n3": {"weight": 1},
            "n4": {"weight": 1},
            "n5": {"weight": 1},
        }
        physical_edges = {
            ("n1", "n2"): {"weight": 1},
            ("n1", "n3"): {"weight": 1},
            ("n2", "n3"): {"weight": 1},
            ("n2", "n4"): {"weight": 1},
            ("n3", "n4"): {"weight": 1},
            ("n3", "n5"): {"weight": 1},
            # ("n1", "n2", 1.0),
            # ("n1", "n3", 1.0),
            # ("n2", "n3", 1.0),
            # ("n2", "n4", 1.0),
            # ("n3", "n4", 1.0),
            # ("n3", "n5", 1.0),
        }

        service_nodes = {"s1","s2","s3","s4"}

        deploy_info = {"s1":"n1",
                       "s2":["n2","n3"],
                       "s3":"n4",
                       "s4":"n5"}
        app_sfc = {"app1": ["s1","s2","s3"],
                   "app2": ["s3","s4"]}
        cptem_info = {
                        "n1": "cptem1",
                        "n2": "cptem1",
                        "n3": "cptem1",
                        "n4": "cptem1",
                        "n5": "cptem1",
                        ("n1", "n2"): "cptem2",
                        ("n1", "n3"): "cptem2",
                        ("n2", "n3"): "cptem2",
                        ("n2", "n4"): "cptem2",
                        ("n3", "n4"): "cptem2",
                        ("n3", "n5"): "cptem2",
                        "s1": "service",
                        "s2": "service_hot_backup",
                        "s3": "service",
                        "s4": "service",
                        ("s1", "s2") : "gen",
                        ("s2", "s3") : "gen",
                        ("s3", "s4") : "gen",
                        "app1" : "app",
                        "app2" : "app",
                        "controller": "con"
                      }
        self.initial_topo(physical_nodes, physical_edges, service_nodes, deploy_info, app_sfc, cptem_info)
        return self


        # pos = nx.spring_layout(self)
        # nx.draw_networkx(self, pos=pos)
        # path_edges = [('s1', 's2'), ('s2', 's3')]

        # new_pos = {}
        # for node in self.nodes():
        #     new_pos[node] = (pos[node][0], pos[node][1] + 0.1)
        # nx.draw_networkx_edges(self, pos=new_pos, edgelist=path_edges, edge_color='red', arrows=True,arrowstyle="-|>",node_size=0)

def dict_equal(dict1, dict2):
    if not isinstance(dict1, dict) or not isinstance(dict2, dict):
        return dict1 == dict2

    if set(dict1.keys()) != set(dict2.keys()):
        return False

    for key in dict1:
        if isinstance(dict1[key], (list, tuple)) and isinstance(dict2[key], (list, tuple)):
            if len(dict1[key]) != len(dict2[key]):
                return False
            for i in range(len(dict1[key])):
                if not dict_equal(dict1[key][i], dict2[key][i]):
                    return False
        elif isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
            if not dict_equal(dict1[key], dict2[key]):
                return False
        elif dict1[key] != dict2[key]:
            return False
    return True
    
if __name__ == '__main__':
    topo = Topo().mlag()
    topo_info = topo.get_topo_state()["service_edges"]
    print(topo_info)

    topo.node_fail("n2")
    topo.update_service_edge()
    topo_info = topo.get_topo_state()["service_edges"]
    print(topo_info)

    topo.node_repair("n2")
    topo.update_service_edge()
    topo_info = topo.get_topo_state()["service_edges"]
    print(topo_info)

    topo.edge_fail(("n2", "n3"))
    topo.update_service_edge()
    topo_info = topo.get_topo_state()["service_edges"]
    print(topo_info)

    topo.edge_repair(("n2", "n3"))
    topo.update_service_edge()
    topo_info = topo.get_topo_state()["service_edges"]
    print(topo_info)

    paths =  topo.path_of_service_edge(("s1", "s2"))


